﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Xml;

using Spencen.Mobile.Markup.Converters;
using Spencen.Mobile.Markup.Extensions;

namespace Spencen.Mobile.Markup
{
    public class XamlReader
    {
        public const string MobileXamlUri = @"http://mobileui.codeplex.com/xaml";

        private static readonly IDictionary<string, Type> _resolvedTypes = new Dictionary<string, Type>();
        private readonly IDictionary<Type, IConverter> _converters = new Dictionary<Type, IConverter>();
        private readonly IDictionary<string, object> _namedElements = new Dictionary<string, object>();
        private readonly IList<IMarkupExtension> _markupExtensions = new List<IMarkupExtension>();

        public string DefaultNamespace { get; set; }
        public string DefaultAssemblyName { get; set; }
        public IDictionary<Type, IConverter> Converters { get { return _converters; } }
        public IList<IMarkupExtension> MarkupExtensions { get { return _markupExtensions; } }

        public XamlReader()
        {
            DefaultNamespace = "Spencen.Mobile.UI.Primitives";
            DefaultAssemblyName = "Spencen.Mobile.UI";

            // This is awful - but will do for now - dynamically load all type converters declared in this assembly and 
            // associate with the type that they convert. Makes heaps of non-extensible assumptions.
            var converters =
                from s in Assembly.GetExecutingAssembly().GetTypes()
                where s.BaseType != null && s.BaseType.IsGenericType && s.BaseType.GetGenericTypeDefinition() == typeof( Converter<> ) && !s.IsInterface && !s.IsAbstract
                select new KeyValuePair<Type, IConverter>( s.BaseType.GetGenericArguments()[ 0 ], (IConverter)Activator.CreateInstance( s ) );

            foreach ( var converter in converters )
                _converters.Add( converter );

            MarkupExtensions.Add( new NullMarkupExtension() );
            MarkupExtensions.Add( new BindingMarkupExtension() );
            MarkupExtensions.Add( new StaticResourceMarkupExtension() );
        }

        public XamlElement Read( XmlReader xmlReader )
        {
            Stack<XamlElementBase> stack = new Stack<XamlElementBase>();
            XamlElementBase currentElement = null;
            XamlElementBase parentElement = null;
            xmlReader.Read();
            while ( !xmlReader.EOF )
            {
                switch ( xmlReader.NodeType )
                {
                    case XmlNodeType.Element:
                        if ( xmlReader.LocalName.IndexOf( "." ) > 0 )
                        {
                            currentElement = new XamlElementProperty( parentElement, xmlReader.LocalName, xmlReader.NamespaceURI );
                            if ( parentElement != null )
                                ((XamlElement) parentElement).Properties.Add( (XamlElementProperty) currentElement );
                        }
                        else
                        {
                            currentElement = new XamlElement( parentElement, xmlReader.LocalName, xmlReader.NamespaceURI );
                            if ( parentElement != null )
                                parentElement.Children.Add( (XamlElement) currentElement );
                        }

                        if ( !xmlReader.IsEmptyElement )
                        {
                            parentElement = currentElement;
                            stack.Push( currentElement );
                        }

                        if ( xmlReader.HasAttributes )
                            xmlReader.MoveToFirstAttribute();
                        else
                            xmlReader.Read();

                        break;

                    case XmlNodeType.Attribute:
                        ProcessAttribute( xmlReader, (XamlElement) currentElement );
                        break;

                    case XmlNodeType.Text:
                        currentElement.Value = xmlReader.Value;
                        break;

                    case XmlNodeType.EndElement:
                        currentElement = stack.Pop();
                        parentElement = stack.Count == 0 ? null : stack.Peek();
                        xmlReader.Read();
                        break;

                    default:
                        xmlReader.Read();
                        break;
                }
            }
            return (XamlElement) currentElement;
        }

        private void ProcessAttribute( XmlReader xmlReader, XamlElement currentElement )
        {
            if ( xmlReader.Prefix == "xmlns" || xmlReader.Name == "xmlns" )
            { /* Ignore */ }
            else
            {
                if ( xmlReader.NamespaceURI == MobileXamlUri )
                {
                    if ( xmlReader.LocalName == "Key" )
                        currentElement.ResourceKey = xmlReader.Value;
                    else if ( xmlReader.LocalName == "Name" )
                        currentElement.ResourceName = xmlReader.Value;
                }
                else
                {
                    var attribute = new XamlAttribute( currentElement, xmlReader.LocalName, xmlReader.NamespaceURI );
                    attribute.Value = xmlReader.Value;
                    currentElement.Attributes.Add( attribute );
                }
            }
            if ( !xmlReader.MoveToNextAttribute() )
                xmlReader.Read();
        }

        public void Resolve( XamlElement rootElement )
        {
            rootElement.Type = GetTypeForElement( rootElement );
            rootElement.Constructor = GetBestMatchingConstructor( rootElement );

            foreach ( var property in rootElement.Properties )
            {
                var memberName = property.MemberName;
                property.PropertyDescriptor = ResolvePropertyType( rootElement.Type, property );
                foreach ( var subChildElement in property.Children )
                    Resolve( subChildElement );
            }

            foreach ( var childElement in rootElement.Children )
            {
                Resolve( childElement );
            }

            foreach ( var childAttribute in rootElement.Attributes )
            {
                childAttribute.PropertyDescriptor = ResolvePropertyType( rootElement.Type, childAttribute );

                if ( childAttribute.PropertyDescriptor == null )
                    throw new InvalidOperationException( string.Format( "Unknown XAML attribute {0} specified within parent {1} element.", childAttribute.Name, rootElement.Name ) );

                if ( childAttribute.Value is string )
                    childAttribute.ConvertedValue = GetConvertedValue( rootElement, childAttribute.PropertyDescriptor.PropertyType, (string)childAttribute.Value );
                else
                    childAttribute.ConvertedValue = childAttribute.Value;
            }
        }

        private Type GetTypeForElement( XamlElement rootElement )
        {
            return ResolveType( rootElement );
        }

        private Type ResolveType( XamlNode rootElement )
        {
            if ( string.IsNullOrEmpty( rootElement.Namespace ) )
                return Type.GetType( string.Format( "{0}.{1}", DefaultNamespace, rootElement.Name ) );
            else if ( rootElement.Namespace == @"http://mobileui.codeplex.com/v1" )
            {
                Type resolvedType = null;
                if ( _resolvedTypes.TryGetValue( rootElement.Name, out resolvedType ) )
                    return resolvedType;

                var assembly = System.Reflection.Assembly.Load( DefaultAssemblyName ); // System.Reflection.Assembly.GetExecutingAssembly();
                var attributes = assembly.GetCustomAttributes( typeof( XamlNamespaceMappingAttribute ), true );
                foreach ( XamlNamespaceMappingAttribute attribute in attributes )
                {
                    var fullTypeName = string.Format( "{0}.{1}", attribute.ClrNamespace, rootElement.Name, false );
                    resolvedType = assembly.GetType( fullTypeName );
                    if ( resolvedType != null )
                    {
                        _resolvedTypes.Add( rootElement.Name, resolvedType );
                        return resolvedType;
                    }
                }
            }

            var splitNamespace = rootElement.Namespace.Split( ',' );
            var typeName = string.Format( "{0}.{1},{2}",
                splitNamespace[ 0 ],
                rootElement.Name,
                splitNamespace.Length == 1 ? DefaultAssemblyName : splitNamespace[ 1 ] );

            return Type.GetType( typeName, true );
        }

        private ConstructorInfo GetBestMatchingConstructor( XamlElement element )
        {
            var constructors = element.Type.GetConstructors( BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.Instance );

            // TODO: Hook for IDrawingHost container using Host as constructor arg

            XamlAttribute[] args;
            var bestConstructor = GetBestMatchingConstructor( constructors, element, out args );

            element.Constructor = bestConstructor;
            element.ConstructorArgs = args.ToList<XamlAttribute>();
            return bestConstructor;
        }

        private ConstructorInfo GetBestMatchingConstructor( ConstructorInfo[] constructors, XamlElement rootElement, out XamlAttribute[] args )
        {
            foreach ( var constructor in constructors.OrderByDescending( c => c.GetParameters().Count() ) )
            {
                var matchingAttributeCount = 0;
                var parameters = constructor.GetParameters();
                foreach ( var parameter in parameters )
                {
                    var matchingAttribute = rootElement.Attributes.FirstOrDefault( a => a.Name.Equals( parameter.Name, StringComparison.OrdinalIgnoreCase ) );
                    if ( matchingAttribute == null )
                        break;
                    else
                        matchingAttributeCount++;
                }
                if ( matchingAttributeCount == parameters.Length ) // Found them all
                {
                    args = new XamlAttribute[ parameters.Length ];
                    var argIndex = 0;
                    foreach ( var parameter in parameters )
                    {
                        var matchingAttribute = rootElement.Attributes.First( a => a.Name.Equals( parameter.Name, StringComparison.OrdinalIgnoreCase ) );
                        args[ argIndex++ ] = matchingAttribute;
                        if ( matchingAttribute.Value is string )
                            matchingAttribute.ConvertedValue = GetConvertedValue( rootElement, parameter.ParameterType, matchingAttribute.Value.ToString() );

                        // Since the attribute is going to be used as a constructor argument we don't need to process it as a property.
                        rootElement.Attributes.Remove( matchingAttribute );
                    }
                    return constructor;
                }
            }
            args = new XamlAttribute[] { };
            return null;
        }

        public PropertyDescriptor ResolvePropertyType( Type parentType, XamlNode propertyNode )
        {
            return TypeDescriptor.GetProperties( parentType ).Find( propertyNode.MemberName, false );
        }

        public object GetConvertedValue( XamlNode rootElement, Type propertyType, string value )
        {
            // TODO: RegEx this
            if ( value.StartsWith( "{" ) && value.EndsWith( "}" ) )
            {
                var separatorIndex = value.IndexOf( ' ' );
                string extensionName;
                string args= null;
                if ( separatorIndex < 0 )
                    extensionName = value.Substring( 1, value.Length - 2 );
                else
                {
                    extensionName = value.Substring( 1, separatorIndex - 1 );
                    args = value.Substring( separatorIndex + 1, value.Length - separatorIndex - 2 );
                }
                var extension = MarkupExtensions.FirstOrDefault( m => m.ExtensionName == extensionName );
                if ( extension == null )
                    throw new InvalidOperationException( string.Format( "Unknown markup extension {0} not found in list of known extensions: {1}", extensionName, string.Join( ", ", MarkupExtensions.Select( m => m.ExtensionName ).ToArray() ) ) );
                else
                    return extension.Resolve( rootElement, propertyType, args );
            }

            var validConverters =
                from c in Converters
                where c.Key.IsAssignableFrom( propertyType )
                orderby c.Key.Name
                select c.Value;

            foreach ( var converter in validConverters )
                if ( converter.CanConvertFromString( value, propertyType ) )
                    return converter.ConvertFromString( value, propertyType );

            return value;
        }
    }
}
